/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file camera2D.cpp
 * @author TooOld2Rock'nRoll
 * @see camera.hpp
 */
/*---- Includes ----*/
#include "camera2D.hpp"

#include "toolbox/debug.h"


/*---- Global Variables ----*/


/*---- Methods Implementation ----*/
/**
 * @brief Process and update the Orthographic Camera View Matrix.
 *
 * @return The used matrix for the Camera2D View.
 */
glm::mat4 Camera2D::getViewMatrix ()
{
    //rebuild view matrix only if any flags are set
    if (this->b_dirty_flag)
    {
        this->m4_view_cash = glm::mat4 (1.f);

        //move the camera focus to the center of the screen
        this->m4_view_cash = glm::translate (this->m4_view_cash, glm::vec3(this->ui_win_width/2.f, this->ui_win_height/2.f, 0.f));

        //in ortho, all vertical lines are parallel, models never change size.
        //so we emulate the desired effect with scale...
        this->m4_view_cash = glm::scale (this->m4_view_cash, glm::vec3 (this->v3_position.z, this->v3_position.z, 1.f));

        //move camera position to the world coordinates
        //camera movement is inverted to the screen movement
        this->m4_view_cash = glm::translate (this->m4_view_cash, glm::vec3 (-this->v3_position.x, -this->v3_position.y, 0.f));

        this->b_dirty_flag = false;
    }//end if (this->b_pos_dirty_flag)

    return this->m4_view_cash;
}//End GetViewMatrix ()


/**
 * @brief Defines the size of the window the camera view have to work with.
 *
 * @param win_width - the width in pixels of the camera frame.
 * @param win_height - the height in pixels of the camera frame.
 */
void Camera2D::setWindowSize (unsigned win_width, unsigned win_height)
{
    _debug (TRACE, "::%s (height=%u, width=%u)", __FUNCTION__, win_height, win_width);

    this->ui_win_height = win_height;
    this->ui_win_width = win_width;
    this->f_win_proportion = (float)win_width / win_height;
}//End setWindowSize ()


/**
 * @brief Defines the size of the word the camera view will bound to.
 *
 * @param max_width - the maximum width in pixels the camera may zoom out.
 * @param max_height - the maximum height in pixels the camera may zoom out.
 */
void Camera2D::setWorldSize (unsigned max_width, unsigned max_height)
{
    _debug (TRACE, "::%s (max_height=%u, max_width=%u)", __FUNCTION__, max_height, max_width);

    this->ui_world_height = max_height;
    this->ui_world_width = max_width;
}//End setWorldSize ()


/**
 * @brief Sets the camera's focus (center point) absolute position in the world.
 *
 * @remark Since a orthographic projection makes all objects parallel to the
 *            vertical axle, z is actually a scale applied to the view, so it can work as pan for the camera.
 *
 * @param position - [x,y] center camera position, [z] scale.
 */
void Camera2D::lookAt (const glm::vec3 &position)
{
    static float half_width = 0.f;
    static float half_height = 0.f;

    _debug (TRACE, "::%s (x=%.2f, y=%.2f, z=%.2f)", __FUNCTION__, position.x, position.y, position.z);

    if (position == this->v3_position)
        return; //avoid rebuilding model

    this->b_dirty_flag = true;
    this->v3_position = position;

    half_width = (this->ui_win_width / 2.f) / this->v3_position.z;
    half_height = (this->ui_win_height / 2.f) / this->v3_position.z;

    this->_visible_world_limits.f_x1 = this->v3_position.x - half_width;
    this->_visible_world_limits.f_y1 = this->v3_position.y - half_height;
    this->_visible_world_limits.f_x2 = this->v3_position.x + half_width;
    this->_visible_world_limits.f_y2 = this->v3_position.y + half_height;
}//End lookAt ()


/**
 * @brief Sets the camera frame to fit to the game window.
 *
 * @param center - <b>true</b> if the world should also be centered after frame set.
 */
void Camera2D::fitToWindow (bool center)
{
    float final_scale = 1.f;

    _debug (TRACE, "::%s (center=%s)", __FUNCTION__, (center ? "true" : "false"));

    //there is a special case when the world size and the screen size are the same
    //for optimization, we check the opposite...
    if (this->ui_win_width != this->ui_world_width ||
            this->ui_win_height != this->ui_world_height)
    {
        if (this->ui_world_width < this->ui_world_height)
            final_scale = (float)this->ui_win_width/this->ui_world_width;
        else
            final_scale = (float)this->ui_win_width/this->ui_world_height;
    }//end if (this->ui_win_width != this->ui_world_width || ...

    this->lookAt ((center ? this->ui_world_width/2.f : this->v3_position.x),
                  (center ? this->ui_world_height/2.f : this->v3_position.y),
                  final_scale);
}//end fitToWindowfinal_scale); ()


/**
 * @brief Tries to keep the camera frame inside the world coordinates.
 *
 * @param[in,out] x - reference to the x coordinate of the camera.
 * @param[in,out] y - reference to the y coordinate of the camera.
 * @param[in,out] w - reference to the width of the camera frame.
 * @param[in,out] h - reference to the height of the camera frame.
 */
void Camera2D::fitToBoundaries (float &x, float &y, float &w, float &h)
{
    static float half_w = 0.f,
                 half_h = 0.f;

    half_w = w / 2.f;
    half_h = h / 2.f;

    if (w > this->ui_world_width)
    {
        x = this->ui_world_width / 2.f;
        w = this->ui_world_width;
        h = this->ui_world_width * this->f_win_proportion; //stop height from growing
    }//end else if (w > this->ui_world_width)
    else if (x - half_w < 0)
        x = half_w;
    else if (x + half_w >= this->ui_world_width)
        x = this->ui_world_width - half_w;

    if (h > this->ui_world_height)
    {
        y = this->ui_world_height / 2.f;
        h = this->ui_world_height;
        w = this->ui_world_height * this->f_win_proportion; //stop width from growing
    }//end else if (w > this->ui_world_width)
    else if (y - half_h < 0)
        y = half_h;
    else if (y + half_h >= this->ui_world_height)
        y = this->ui_world_height - half_h;
}//end fitToBoundaries ()

/**
 * @brief A more practical way to set the camera frame.
 *
 * This method will do its best to keep the frame inside the given boundaries previously set as max width/height.<br>
 * If this behavior is undesirable, you may need to build your own method of focusing the camera.
 *
 * @param x_center - the x coordinate in pixels for the world coordinate to focus the camera.
 * @param y_center - the y coordinate in pixels for the world coordinate to focus the camera.
 * @param height - the height in pixels for the window in world coordinate to frame the camera.
 */
void Camera2D::frameByHeight (float x_center, float y_center, float height)
{
    static float new_w = 0.f;

    _debug (TRACE, "::%s (x_center=%.2f, y_center=%.2f, height=%.2f)", __FUNCTION__, x_center, y_center, height);

    new_w = height / this->f_win_proportion;
    this->fitToBoundaries (x_center, y_center, new_w, height);

    this->lookAt (x_center, y_center, this->ui_win_height / height);
}//End setViewWindowByHeight ()


/**
 * @brief A more practical way to set the camera frame.
 *
 * This method will do its best to keep the frame inside the given boundaries previously set as max width/height.<br>
 * If this behavior is undesirable, you may need to build your own method of focusing the camera.
 *
 * @param x_center - the x coordinate in pixels for the world coordinate to focus the camera.
 * @param y_center - the y coordinate in pixels for the world coordinate to focus the camera.
 * @param width - the width in pixels for the window in world coordinate to frame the camera.
 */
void Camera2D::frameByWidth (float x_center, float y_center, float width)
{
    static float new_h = 0.f;

    _debug (TRACE, "::%s (x_center=%.2f, y_center=%.2f, width=%.2f)", __FUNCTION__, x_center, y_center, width);

    new_h = width / this->f_win_proportion;
    this->fitToBoundaries (x_center, y_center, width, new_h);

    this->lookAt (x_center, y_center, this->ui_win_width / width);
}//End setViewWindowByWidth ()

